﻿using RockBus.Configuration.Interfaces;
using RockBus.DataContracts;
using RockBus.Hosting.Interfaces;
using RockBus.Logging.Interfaces;
using RockBus.Routing.Interfaces;
using RockBus.Utilities;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.Threading;
using System.Threading.Tasks;
using ServiceHostEntry = RockBus.Utilities.MutableTuple<bool, RockBus.DataContracts.PublisherReceivePortInfo, System.ServiceModel.ServiceHostBase>;
using ServiceHostKey = System.Tuple<string, string>;
using RockBus.Services;

namespace RockBus.Hosting
{
    public class SelfHostedServiceHostManager : IServiceHostManager
    {
        public SelfHostedServiceHostManager(
            ILogger logger,
            IReadOnlyConfiguration configuration,
            IReceivePortServiceHostFactory receivePortServiceHostFactory)
        {
            this.DynamicServiceHostDictionary = new ConcurrentDictionary<ServiceHostKey, ServiceHostEntry>();
            this.StaticServiceHosts = new List<ServiceHostBase>();

            this.Logger = logger;
            this.Configuration = configuration;
            this.ReceivePortServiceHostFactory = receivePortServiceHostFactory;
        }

        private void Configuration_OnChanged(object sender, EventArgs e)
        {
            this.UpdateServiceHosts(this.Configuration.PublisherConfiguration);
        }

        private ILogger Logger { get; set; }

        private IReadOnlyConfiguration Configuration { get; set; }

        private IReceivePortServiceHostFactory ReceivePortServiceHostFactory { get; set; }

        private PublisherConfiguration PublisherConfiguration { get; set; }

        private ConcurrentDictionary<ServiceHostKey, ServiceHostEntry> DynamicServiceHostDictionary { get; set; }

        private List<ServiceHostBase> StaticServiceHosts { get; set; }

        private int _guard;

        public void Start()
        {
            this.Logger.LogInfo(MethodBase.GetCurrentMethod(), "ServiceHostManager starting.");
            this.Configuration.OnChanged += Configuration_OnChanged;
        }

        public async void UpdateServiceHosts(PublisherConfiguration publisherConfiguration)
        {
            if ((publisherConfiguration == null) ||
                (PublisherConfiguration.IsEqual(this.PublisherConfiguration, publisherConfiguration)))
            {
                return;
            }

            if (Interlocked.CompareExchange(ref this._guard, 1, 0) == 1)
            {
                this.Logger.LogWarning(MethodBase.GetCurrentMethod(), "Another method was already running, skipping this time.");
                return;
            }

            this.PublisherConfiguration = publisherConfiguration.Clone<PublisherConfiguration>();

            this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "Updating ServiceHosts.");

            try
            {
                // Unmark all ServiceHost
                foreach (var entry in this.DynamicServiceHostDictionary.Values.ToArray())
                {
                    entry.Item1 = false;
                }

                var taskList = new List<Task>();

                int active = 0, opening = 0;
                foreach (var publisher in publisherConfiguration.EvaluationElementDictionary.Values)
                {
                    foreach (var receivePortInfo in publisher.InfoDictionary.Values)
                    {
                        // Ignore any receive ports for which the Endpoint or Address is null or empty
                        if ((receivePortInfo.ReceiveEndpointInfo == null) || (string.IsNullOrEmpty(receivePortInfo.ReceiveEndpointInfo.Address)))
                        {
                            continue;
                        }

                        var key = new Tuple<string, string>(publisher.Id, receivePortInfo.Id);
                        ServiceHostEntry entry;
                        if (this.DynamicServiceHostDictionary.TryGetValue(key, out entry))
                        {
                            // Mark the entry
                            entry.Item1 = true;
                            ++active;
                        }
                        else
                        {
                            ++opening;
                            var serviceHost = this.ReceivePortServiceHostFactory.CreateServiceHost(receivePortInfo);
                            var task = this.OpenServiceHostAsync(serviceHost);
                            task = task.ContinueWith((t) =>
                            {
                                if (t.IsCompleted)
                                {
                                    var entry2 = new MutableTuple<bool, PublisherReceivePortInfo, ServiceHostBase>(true, receivePortInfo.GetPublisherReceivePortInfo(), serviceHost);
                                    this.DynamicServiceHostDictionary.TryAdd(key, entry2);
                                }
                                else
                                {
                                    // If opening the ServiceHost fails, we log the error and hope for the best the next time
                                    //  We don't add it to the dictionary, so it should show up next time as a new ServiceHost to be created.
                                    this.Logger.LogError(MethodBase.GetCurrentMethod(),
                                        "Error opening ServiceHost for PublisherId: {0}, ReceivePortId: {1}. EXCEPTION: {2}",
                                        publisher.Id,
                                        receivePortInfo.Id,
                                        t.Exception);
                                }
                            });
                            taskList.Add(task);
                        }
                    }
                }

                this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "ServiceHosts Active: {0}; Opening: {1}", active, opening);

                int removed = 0;
                foreach (var kvp in this.DynamicServiceHostDictionary.ToArray())
                {
                    if (!kvp.Value.Item1)
                    {
                        ServiceHostEntry entry = null;
                        if (this.DynamicServiceHostDictionary.TryRemove(kvp.Key, out entry))
                        {
                            ++removed;
                            var task = Task.Factory.FromAsync(this.BeginClose(entry.Item3, 60000), this.EndClose);
                            taskList.Add(task);
                        }
                    }
                }

                this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "ServiceHosts Removed: {0};", removed);

                // Wait for all service hosts to have been created
                if (taskList.Count > 0)
                {
                    await Task.Factory.ContinueWhenAll(taskList.ToArray(), (ta) => { });
                    Task.WaitAll(taskList.ToArray());
                }
            }
            catch (Exception exc)
            {
                this.Logger.Log(MethodBase.GetCurrentMethod(), exc);
            }
            finally
            {
                this._guard = 0;
            }
        }

        /// <summary>
        /// Takes full ownership of the ServiceHost's lifetime.
        /// </summary>
        /// <param name="publisherReceivePortInfo"></param>
        /// <param name="serviceHost"></param>
        /// <returns></returns>
        public void RegisterDynamicServiceHost(ReceivePortInfo receivePortInfo, ServiceHostBase serviceHost)
        {
            this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "Registering ReceivePort ServiceHosts: {0}", receivePortInfo.ToString());

            var key = new ServiceHostKey(receivePortInfo.PublisherInfo.Id, receivePortInfo.Id);
            var entry = new MutableTuple<bool, PublisherReceivePortInfo, ServiceHostBase>(true, null, serviceHost);
            var added = this.DynamicServiceHostDictionary.TryAdd(key, entry);

            var dynamicServiceHost = serviceHost as IReceivePortServiceHost;
            if (added && (dynamicServiceHost != null))
            {
                entry.Item2 = dynamicServiceHost.PublisherReceivePortInfo;
            }
        }

        public void RegisterStaticServiceHost(ServiceHostBase serviceHost)
        {
            this.StaticServiceHosts.Add(serviceHost);
        }

        private Task OpenServiceHostAsync(ServiceHostBase serviceHost)
        {
            return Task.Factory.FromAsync(this.BeginOpen(serviceHost, this.Configuration.InternalConfiguration.ServiceHostOpenTimeoutMs), this.EndOpen);
        }

        private IAsyncResult BeginOpen(ServiceHostBase serviceHost, int timeoutMs)
        {
            return serviceHost.BeginOpen(TimeSpan.FromMilliseconds(timeoutMs), this.EndOpen, serviceHost);
        }

        private void EndOpen(IAsyncResult asyncResult)
        {
            var sh = asyncResult.AsyncState as ServiceHostBase;
        }

        private IAsyncResult BeginClose(ServiceHostBase serviceHost, int timeoutMs)
        {
            return serviceHost.BeginClose(TimeSpan.FromMilliseconds(timeoutMs), this.EndClose, serviceHost);
        }

        private void EndClose(IAsyncResult asyncResult)
        {
            var sh = asyncResult.AsyncState as ServiceHostBase;
        }
    }
}